AWS CDK(Cloud Development Kit )で、CodePipeline、CodeCommit、CodeBuildを使用した開発環境を作ってみました
1 はじめに
CX事業本部の平内(SIN)です。
今回は、AWS CDKで、CodePipeline、CodeCommit、CodeBuildを使用した開発環境を作ってみました。デプロイされるのは、Lambdaファンクションのみです。
CodeCommitのリポジトリで、developブランチをコミットすると、dev環境のLambdaが更新され、masterブランチでprd環境が更新されるようになってます。
2 リポジトリ作成
最初に、CodeCommitでリポジトリを作成します。
スタックの中で作成することも可能ですが、その場合、スタックの削除でリポジトリも消えてしまうので、ちょっと運用上まずいかと思います。
$ aws codecommit create-repository --repository-name SampleRepo --repository-description "My sample repository"
3 AWS CDK
AWS CDKによる作業手順は、以下のとおりです。
(1) プロジェクト作成
作業用ディレクトリを作成して、その中で、initコマンドを実行します。
$ mkdir my-cicd;cd my-cicd $ cdk init app --language=typescript
(2) モジュールインストール
使用するモジュールをインストールします。
$ npm install @aws-cdk/aws-lambda $ npm install @aws-cdk/aws-codepipeline $ npm install @aws-cdk/aws-codepipeline-actions $ npm install @aws-cdk/aws-codebuild $ npm install @aws-cdk/aws-iam $ npm install @aws-cdk/aws-codecommit
(3) DMYプロジェクト
これは、スタックの中でLambda関数を新しく生成するのですが、そのCodeアセットのためのダミーです。(AWS CDKには、直接関係ないです)
$ mkdir dmy $ echo "SAMPLE" > dmy/README.md
(4) コード作成
スタックのコードは、lib/my-cicd-stack.tsを編集します。(細部は、後述)
(5) ビルド・デプロイ
tscでビルドします。
$ npm run build
エラーがあれば、synthコマンドでもある程度のエラーは確認できます。
$ cdk synth
deployコマンドでデプロイします。
$ cdk deploy
4 コード
スタック作成のためのコードは、以下のとおりです。作業単位をメソッドとして定義できるので、非常に見通しが良いように思います。
devとprdは、ステージ変数(stage)にして、forEachで回しています。
createId()は、スタック間でも一意である必要のあるID生成で、tagをその識別子としています。一方、cerateName()は、リソースの名前生成で、AWSコンソール上での視認性を高めるため、自動生成(スタック名+XXX)では無く、tag + stageで積極的に指定してみました。
lib/my-cicd-stack.ts
// 対象Lambda // (develop => targetFunctionName_dev) // (master => targetFunctionName_prd) const targetFunctionName = "sample-function"; // リポジトリ名 repositoryName const repositoryName = "SampleRepo" // 識別するためのタグ const tag = "my-cicd"; import cdk = require('@aws-cdk/core'); import * as lambda from '@aws-cdk/aws-lambda'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as iam from '@aws-cdk/aws-iam'; import * as codecommit from '@aws-cdk/aws-codecommit'; export class MyCiCdStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // リポジトリの取得 const repo = this.getRepository(repositoryName); // 2つのステージ(prd/dev)分のリソースを生成する ["prd","dev"].forEach( stage => { // デプロイ先のLambdaを仮に作成する // buildspec.ymlからのデプロイは、updateのみ許可する const targetFunction = this.createFunction(stage); // プロジェクトの生成 const project = this.createProject(targetFunction, stage); // パイプラインの生成 const sourceOutput = new codepipeline.Artifact(); // 対象ブランチ(prd:master dev:developとなる) const branch = (stage=='dev')?'develop':'master'; new codepipeline.Pipeline(this, this.createId('Pipeline',stage), { pipelineName: this.createName(stage), stages: [{ stageName: 'Source', actions: [ this.createSourceAction(repo, branch, sourceOutput) ], }, { stageName: 'Build', actions: [ this.createBuildAction(project, sourceOutput) ], }, ], }); }) } //**************************************************** */ // idの生成(tag + name + stage) //**************************************************** */ private createId(name:string, stage: string): string { return tag + '-' + name + '-' + stage } //**************************************************** */ // 名前の生成(tag + stage) //**************************************************** */ private createName(stage: string): string { return tag + '_' + stage } //**************************************************** */ // Lambdaファンクションの生成 //**************************************************** */ private createFunction(stage: string):lambda.Function { return new lambda.Function(this, this.createId('Target', stage), { functionName: targetFunctionName + '_' + stage, code: lambda.Code.asset('dmy'), // コードは仮のもの handler: 'index.handler', runtime: lambda.Runtime.NODEJS_10_X, }); } //**************************************************** */ // プロジェクトの生成 //**************************************************** */ private createProject(targetFunction:lambda.Function, stage: string):codebuild.PipelineProject { const project = new codebuild.PipelineProject(this, this.createId('Project',stage), { projectName: this.createName(stage), environment: { // 環境変数(関数名及び、ステージ)をbuildspec.ymlに送る environmentVariables: { FUNCTION_NAME: { type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, value: targetFunction.functionName, }, STAGE: { type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, value: stage, } }, }, }); // buildspc.ymlからLambdaをupdateするため、パーミッションを追加 project.addToRolePolicy(new iam.PolicyStatement({ resources: [targetFunction.functionArn], actions: ['lambda:UpdateFunctionCode', 'lambda:UpdateFunctionConfiguration',] } )); return project; } //**************************************************** */ // リポジトリの取得 //**************************************************** */ private getRepository(repositoryName:string):codecommit.Repository { return codecommit.Repository.fromRepositoryName(this, this.createId("repo",""), repositoryName) as codecommit.Repository; // 新規にリポジトリを作成する場合(注:スタックの削除でリポジトリも削除される) // return new codecommit.Repository(this, this.createId('Repository',"") ,{ // repositoryName: repositoryName, // }); } //**************************************************** */ // CodePipelineのソースアクション(CodeCommit)の生成 //**************************************************** */ private createSourceAction(repo:codecommit.Repository, branch: string, sourceOutput: codepipeline.Artifact): codepipeline_actions.CodeCommitSourceAction { return new codepipeline_actions.CodeCommitSourceAction ({ actionName: 'CodeCommit', repository: repo, branch: branch, output: sourceOutput, }); } //**************************************************** */ // CodePipelineのビルドアクション(CodeBuild) の生成 //**************************************************** */ private createBuildAction(project: codebuild.IProject, sourceOutput: codepipeline.Artifact) { return new codepipeline_actions.CodeBuildAction({ actionName: 'CodeBuild', project, input: sourceOutput, outputs: [new codepipeline.Artifact()] }); } }
5 リソース
作成されているリソースを少し確認しておきます。
CodePipeline
CodeBuild
Lambda
ビルド実行の様子です。
6 buildspec.yml
こちらは、コミットされるコードのトップに置かれたbuildspec.ymlです。CodeBuildで実行され、LambdaをUpdateしています。
完全に個人的な趣味ですが、ここに、Lambdaの環境変数などのパラメータを集約することで、コード側で管理できるようにしています。
version: 0.2 env: variables: DESCRIPTION: sample function RUN_TIME: nodejs10.x MEMORY: 128 TIMEOUT: 5 HANDLER: index.handler ENV: TZ=Asia/Tokyo,NODE_ENV= # NODE_ENV=$STAGE ZIP_FILE: /tmp/upload.zip phases: pre_build: commands: - yarn global add typescript - yarn install - build: commands: - tsc - npm test - zip -r -q ${ZIP_FILE} * - aws lambda update-function-code --function-name $FUNCTION_NAME --zip-file fileb://$ZIP_FILE --publish - aws lambda update-function-configuration --function-name $FUNCTION_NAME --environment Variables={$ENV$STAGE} --memory-size $MEMORY --runtime $RUN_TIME --description "$DESCRIPTION" --timeout $TIMEOUT --handler $HANDLER post_build: commands: - echo Deploy completed
7 最後に
すっかり、AWS CDKが気に入ってしまいました。恥ずかしながら、もともと、CloudFormationのテンプレートが得意なわけでは決して無いので、AWS CDKの生成するテンプレートはどうなの?と言われると正直返事に詰まります。
とりあえず、ベストプラクティスによる自動生成に頼りながら、順次、勉強したいと思います。